#ifndef AEYECLIENTS_AEYESENSORSDK_INCLUDEDEV_QUEUEMANAGER_HPP_
#define AEYECLIENTS_AEYESENSORSDK_INCLUDEDEV_QUEUEMANAGER_HPP_

#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <queue>
#include <type_traits>

namespace aeye {

/// An enum to control the behavior of \a QueueManager::push.
enum class QueueType : uint8_t {
  Waiting, ///< Wait until the queue has room to push.
  Overwriting, ///< Do not wait until there is room in the queue, overwrite the oldest element instead.
  Unbounded, ///< Place no limits on the length of the queue.
};

namespace detail {

template<QueueType q>
using queue_constant = std::integral_constant<QueueType, q>;

/// An internal wrapper class that provides most of the public API of \a QueueManager.
/// \sa QueueManager
/// \tparam T The data type in the queue.
template<typename T>
class QueueManagerInternal {
 protected:
  std::queue<T> queue_{};
  const std::atomic<bool> &is_running_;
  const size_t max_queue_size_;

  mutable std::mutex m_;
  std::condition_variable cv_;

  explicit QueueManagerInternal(const std::atomic<bool> &is_running, size_t max);
  ~QueueManagerInternal() = default;

 public:

  /// \brief Remove all elements from the underlying queue.
  void clean() noexcept;

  /// \brief Pop a \a T from the queue.
  /// \return The \a T located at of the queue. Note that the element is automatically removed when popped.
  T pop();

  /// \brief Check if the queue is empty.
  /// \return True if the underlying queue is empty, otherwise false.
  bool empty() const;

  /// \brief Check the size of the underlying queue.
  /// \return The size of the underlying queue.
  size_t size() const;

  /// \brief Check the element at the front of the queue.
  /// \return An immutable reference to the element at the front of the queue.
  const T &front() const;

  /// Check the element at the back of the queue.
  /// \return An immutable reference to the element at the back of the queue.
  const T &back() const;

};

}

/// A wrapper class over an \a std::queue<T> that locks internally to allow accessing from multiple threads.
/// Note that this class inherits from \a QueueManagerInternal to avoid instantiation unnecessary templates, so the
/// majority of the API is there.
/// \sa detail::QueueManagerInternal
/// \tparam T The data type contained in the queue.
/// \tparam Q A QueueType enum class member to control the behavior of \a push.
template<typename T, QueueType Q>
class QueueManager final : public detail::QueueManagerInternal<T> {
  using qt = detail::queue_constant<Q>;
  using detail::QueueManagerInternal<T>::m_;
  using detail::QueueManagerInternal<T>::queue_;
  using detail::QueueManagerInternal<T>::max_queue_size_;
  using detail::QueueManagerInternal<T>::cv_;
  using detail::QueueManagerInternal<T>::is_running_;

 public:

  /// Parametrized constructor for the QueueManager.
  /// \param is_running A reference to the is_running variable in the Reader class.
  /// \param max The maximum size to allow in the queue. The maximum is unused for a \a QueueType::Unbounded queue.
  explicit QueueManager(const std::atomic<bool> &is_running, size_t max)
      : detail::QueueManagerInternal<T>(is_running, max) {}

  /// \brief Wait until the queue has capacity before returning, or return early in the case of an error.
  /// \details Note that this method is only available to be called when not using a \a QueueType::Unbounded queue.
  /// \tparam B SFINAE parameter, should NOT be manually instantiated.
  /// \return False if the queue stops running while waiting, otherwise true.
  template<bool B = !std::is_same<qt, detail::queue_constant<QueueType::Unbounded>>::value>
  std::enable_if_t<B, bool> wait_to_push() noexcept;

  /// \brief Push a \a T into the back of the queue.
  /// \details This method is controlled by the \a QueueType enum class.
  /// \param data The object to be moved into the queue.
  template<bool B = std::is_same<qt, detail::queue_constant<QueueType::Unbounded>>::value, std::enable_if_t<B, int> = 0>
  void push(T &&data);

  template<bool B = std::is_same<qt, detail::queue_constant<QueueType::Waiting>>::value, std::enable_if_t<B, long> = 0>
  void push(T &&data);

  template<bool B = std::is_same<qt, detail::queue_constant<QueueType::Overwriting>>::value, std::enable_if_t<B, bool> = 0>
  void push(T &&data);

};

template<typename T, QueueType Q>
template<bool B>
inline std::enable_if_t<B, bool> QueueManager<T, Q>::wait_to_push() noexcept {
  // it doesn't make sense to be able to wait for the capacity of an unbounded queue so that def. is disabled
  // with SFINAE here.
  std::unique_lock<decltype(m_)> lk{m_};
  while (queue_.size() >= max_queue_size_) {
    cv_.wait_for(lk, std::chrono::milliseconds(100));
    if (!is_running_) {
      lk.unlock();
      cv_.notify_all();
      return false;
    }
  }
  return true;
}

template<typename T, QueueType Q>
template<bool B, std::enable_if_t<B, int>>
inline void QueueManager<T, Q>::push(T &&data) {
  std::unique_lock<decltype(m_)> lk{m_};
  queue_.push(std::move(data));
  lk.unlock();
  cv_.notify_all();
}

template<typename T, QueueType Q>
template<bool B, std::enable_if_t<B, long>>
inline void QueueManager<T, Q>::push(T &&data) {
  std::unique_lock<decltype(m_)> lk{m_};
  while (queue_.size() >= max_queue_size_) {
    cv_.wait_for(lk, std::chrono::milliseconds(100));
    if (!is_running_) {
      lk.unlock();
      cv_.notify_all();
      return;
    }
  }
  queue_.emplace(std::move(data));
  lk.unlock();
  cv_.notify_all();
}

template<typename T, QueueType Q>
template<bool B, std::enable_if_t<B, bool>>
inline void QueueManager<T, Q>::push(T &&data) {
  std::unique_lock<decltype(m_)> lk{m_};
  while (queue_.size() > max_queue_size_) {
    queue_.pop();
  }
  queue_.push(std::move(data));

  lk.unlock();
  cv_.notify_all();
}

}
#endif //AEYECLIENTS_AEYESENSORSDK_INCLUDEDEV_QUEUEMANAGER_HPP_
